
#include <JSL.hpp>

#include "../Graphics.hpp"
#include "../Material.hpp"
#include "../ShaderManager.hpp"
#include "../../debug.hpp"
#include "../../math.hpp"
#include "../../utils/JSLConvert.hpp"
#include "Animation.hpp"
#include "Mesh.hpp"

#include "Bone.hpp"

namespace zen {

	namespace video {

		namespace model {


			Bone::Bone() {
				init();
			}

			Bone::Bone(const Bone &other) {
				init();
				*this = other;
			}

			Bone::~Bone() {
				free(false);
			}

			Bone& Bone::operator = (const Bone &other) {
				name = other.name;
				parentName = other.parentName;
				materialName = other.materialName;
				localView = other.localView;
				globalView = other.globalView;
				animation = other.animation;
				index = other.index;
				parentIndex = other.parentIndex;
				materialIndex = other.materialIndex;
				meshIndex = other.meshIndex;
				animationIndex = other.animationIndex;
				return *this;
			}

			void Bone::init() {
				name = "";
				parentName = "";
				materialName = "";
				localView = glm::mat4();
				globalView = glm::mat4();
				animation = AnimationFramer();
				index = -1;
				parentIndex = -1;
				meshIndex = -1;
				animationIndex = -1;
				materialIndex = -1;
			}

			void Bone::free(bool reinitialize) {
				if (reinitialize)
					init();
			}

			void Bone::setAnimationFrame(Animation** animations, glm::uint start, glm::uint end, glm::uint position, AnimType type, glm::float32 speed) {
				animation.setStart(start);
				animation.setEnd(end);
				animation.setType(type);
				animation.setPosition(position);
				animation.setSpeed(speed);
				if (animationIndex >= 0)
					animations[animationIndex]->refreshStartEndKeyIndexes(animation);
			}

			void Bone::update(int interval, Bone** bones, Animation** animations) {
				animation.update(interval);
				localView = animations[animationIndex]->computeKey(animation);
				globalView = parentIndex >= 0 ? bones[parentIndex]->globalView * localView : localView;
			}

			void Bone::draw(const glm::mat4 &parentView, ShaderManager* materialShaderManager, Mesh** meshes, Material* materials) const {
				if (meshIndex >= 0) {
					const model::Mesh* mesh = meshes[meshIndex];
					if (materialIndex >= 0) {
						glm::mat4 view = parentView * globalView;
						const Material &material = materials[materialIndex];
						ShaderProgram* shader = materialShaderManager == nullptr ? nullptr : materialShaderManager->get(material.name);
						if (shader == nullptr) {
							ShaderManager::useDefault();
							glLoadMatrixf(glm::value_ptr(view));
							glActiveTexture(GL_TEXTURE0);
							material.activate();
							mesh->draw();
						} else {
							shader->use();
							GLint uniformLocation = shader->uniforms.getLocation(ShaderUniforms::SAMPLER_2D_0);
							if (uniformLocation >= 0) {
								glUniform1i(uniformLocation, 0);
								glActiveTexture(GL_TEXTURE0);
								glBindTexture(GL_TEXTURE_2D, material.texture->getHandle());
							}
							uniformLocation = shader->uniforms.getLocation(ShaderUniforms::MODEL_VIEW_PROJECTION_MATRIX);
							if (uniformLocation >= 0)
								glUniformMatrix4fv(uniformLocation, 1, GL_FALSE, glm::value_ptr(Graphics::getProjection() * view));
							uniformLocation = shader->uniforms.getLocation(ShaderUniforms::MODEL_VIEW_MATRIX);
							if (uniformLocation >= 0)
								glUniformMatrix4fv(uniformLocation, 1, GL_FALSE, glm::value_ptr(view));
							uniformLocation = shader->uniforms.getLocation(ShaderUniforms::PROJECTION_MATRIX);
							if (uniformLocation >= 0)
								glUniformMatrix4fv(uniformLocation, 1, GL_FALSE, glm::value_ptr(Graphics::getProjection()));
							mesh->draw();
						}
					}
				}
			}

			bool Bone::loadResource(jsl::Node* node, std::vector<Mesh*> &meshes, std::vector<Animation*> &animations) {
				zen_call("Bone.loadResource(jsl::Node*)");
				if (node == NULL) {
					zen_log("Error: Invalid resource, must not be null.");
					return false;
				}

				free();

				try {
					this->name = node->at("name")->value();
					this->parentName = node->at("parent")->value();
					this->materialName = node->at("material")->value();

					this->globalView = glm::mat4();
					utils::JSLConvert::load(this->localView, node->at("view"));
					this->localView = glm::transpose(this->localView);

					jsl::Node* meshNode = node->at("mesh");
					if (meshNode != NULL) {
						Mesh* mesh = new Mesh();
						if (mesh->loadResource(meshNode)) {
							meshIndex = (glm::sint)meshes.size();
							meshes.push_back(mesh);
						} else {
							delete mesh;
							mesh = NULL;
							zen_log("Warning: Could not load mesh resource.");
						}
					}

					jsl::Node* animNode = node->at("animation");
					if (animNode != NULL) {
						Animation* animation = new Animation();
						if (animation->loadResource(animNode)) {
							animationIndex = (glm::sint)animations.size();
							animations.push_back(animation);
							this->animation = animation->framer;
						} else {
							delete animation;
							animation = NULL;
							zen_log("Warning: Could not load animation resource.");
						}
					}
				} catch (const std::exception &e) {
					zen_log("Error: " + std::string(e.what()));
					return false;
				}
				return true;
			}


		} // namespace model

	} // namespace video

} // namespace zen
